const { generateToken, verifyToken } = require('./../../helpers/jwt');
const generateSalt = require('../../helpers/generate-salt');
const { MD5, enc } = require('crypto-js');
const { checkTheEmail, checkThePhone, validateField } = require('../../helpers/validator');
const UserSafe = require('../../models/user/user-safe');
const User = require('../../models/user/user');
const db = require('../../helpers/db');

/**
 * 设置授权token
 * @param {*} ctx koa上下文
 * @param {*} data 生成token的数据
 * @param {boolean} remember 是否生成刷新token
 */
function setAuthorization(ctx, data, remember = false) {
    const token = generateToken(data);
    ctx.set('authorization', token);
    const { redis } = ctx;
    redis.set(`access_token:${data.username}`, token, 'EX', 30 * 60);
    if (remember) {
        const expire = 30 * 24 * 60 * 60; // 30天过期
        const refreshToken = generateToken(data, expire);
        redis.set(`refresh_token:${data.username}`, refreshToken, 'EX', expire);
        ctx.body = {
            refreshToken
        };
    }
}

/**
 * 更具账号查找用户信息
 * @param {*} account 登录账户（手机号/邮箱/用户名）
 */
async function findUserByAccount(account) {
    if (checkTheEmail(account)) {
        // 邮箱登录
        return findUserByCondition({
            email: account
        });
    } else if (checkThePhone(account)) {
        // 手机号登录
        return findUserByCondition({
            phone: account
        });
    } else {
        // 用户名登录
        return findUserByCondition({
            username: account
        });
    }
}

/**
 * 登录
 * @param {*} ctx koa上下文
 */
async function login(ctx) {
    const data = ctx.request.body;
    const { account = '', password: inputPass = '', remember } = data;
    validateField({
        account,
        password: inputPass
    });
    const result = await findUserByAccount(account);
    // 没有查询到数据说明用户不存在
    if (!result) {
        ctx.throw(500, '用户不存在');
        return false;
    }
    const { salt, user, password, username } = result;

    // 验证密码是否正确
    if (password === MD5(inputPass, salt).toString(enc.Hex)) {
        setAuthorization(
            ctx,
            {
                id: user,
                username
            },
            remember
        );
        ctx.status = remember ? 200 : 204;
    } else {
        ctx.throw(500, '密码错误');
    }
}

/**
 * 创建一个用户
 * @param  {object} data 用户注册数据
 */
async function createUser(data) {
    const transaction = await db.transaction();
    const { nick = 'mall_' + generateSalt(4) } = data;
    const safeData = { ...data };
    delete safeData.nick;
    try {
        const { id } = await User.create(
            {
                nick
            },
            {
                transaction
            }
        );
        const username = 'm_' + Date.now();
        await UserSafe.create(
            {
                user: id,
                username,
                ...safeData
            },
            {
                transaction
            }
        );
        await transaction.commit();
        return Promise.resolve({
            id,
            username
        });
    } catch (error) {
        await transaction.rollback();
        return Promise.reject(error.message || '用户创建失败');
    }
}

/**
 * 通过手机号/邮箱/用户名等查找用户
 * @param {object} condition 查询条件
 * @returns {Promise<any>} 用户安全信息
 */
async function findUserByCondition(condition) {
    return UserSafe.findOne({
        attributes: ['salt', 'user', 'password', 'username'],
        where: {
            ...condition
        }
    });
}

/**
 * 注册
 * @param {*} ctx koa上下文
 */
async function register(ctx) {
    const { nick = '', password = '', phone = '', code = '' } = ctx.request.body;
    const { redis } = ctx;
    validateField({ password, phone, code });
    // 验证手机短信验证码
    const realCode = await redis.get(`send_code:${phone}`);
    redis.del(`send_code:${phone}`);
    if (code !== realCode) {
        throw new Error('验证码不正确');
    }

    // 创建用户
    const { id, username } = await createUser({ nick, phone, password });

    // 设置授权信息
    setAuthorization(ctx, {
        id,
        username
    });
    ctx.status = 201;
}

/**
 * 获取用户信息
 * @param {*} ctx koa上下文
 */
async function getUser(ctx) {
    const { id } = ctx.state.user;
    const user = await User.findOne({
        where: {
            id
        }
    });
    ctx.body = user;
}

/**
 * 过滤出合法数据
 * @param {object} data 数据
 */
function filterLegalProps(data) {
    const legalProps = ['nick', 'birthday', 'gender', 'avatar'];
    const result = {};
    for (const key in data) {
        if (legalProps.includes(key)) {
            result[key] = data[key];
        }
    }
    return result;
}

/**
 * 更新用户信息
 * @param {*} ctx koa上下文
 */
async function patchUser(ctx) {
    const { id } = ctx.state.user;
    const data = filterLegalProps(ctx.request.body);
    validateField(data);
    const count = await User.update(
        {
            ...data
        },
        {
            where: {
                id
            }
        }
    );
    if (count === 0) {
        throw new Error('更新失败');
    }
    ctx.status = 205;
}

/**
 * 实名认证
 * @param {*} ctx koa上下文
 */
async function verified(ctx) {
    const { id } = ctx.state.user;
    const { realName, IDCard } = ctx.request.body;
    validateField({ realName, IDCard });
    // TODO: 实名认证API

    const count = await UserSafe.update(
        {
            realName,
            IDCard
        },
        {
            where: {
                user: id
            }
        }
    );
    if (count === 0) {
        throw new Error('实名认证失败');
    }
    ctx.status = 205;
}

/**
 * 通过手机短信登录
 * @param {*} ctx koa上下文
 */
async function loginByPhone(ctx) {
    const { phone, code } = ctx.request.body;
    validateField({ phone, code });
    if (!checkThePhone(phone)) {
        ctx.throw(400, '请输入正确的手机号');
        return false;
    }
    // TODO: 验证手机验证码

    const { user } = await findUserByCondition({
        phone
    });
    setAuthorization(ctx, {
        id: user
    });
    ctx.status = 204;
}

/**
 * 验证手机验证码
 * @param {*} ctx koa上下文
 */
async function validateCode(ctx) {
    const { phone, code } = ctx.request.body;
    validateField({ phone, code });
    if (!checkThePhone(phone)) {
        ctx.throw(400, '请输入正确的手机号');
        return false;
    }
    const { redis } = ctx;
    const realCode = await redis.get(phone);
    redis.del(phone);
    if (code === realCode) {
        ctx.status = 204;
    } else {
        ctx.throw(500, '验证码错误');
    }
}

/**
 * 发送手机验证码
 * @param {*} ctx koa上下文
 */
async function sendCode(ctx) {
    const { phone } = ctx.request.body;
    validateField({ phone });
    if (!checkThePhone(phone)) {
        ctx.throw(400, '请输入正确的手机号');
        return false;
    }
    const { redis } = ctx;
    const oldCode = await redis.get(`send_code:${phone}`);
    const ttl = await redis.ttl(`send_code:${phone}`);
    const expire = 5 * 60; // 有效期5分钟
    // 降低发送频率，间隔一分钟以上发送验证码
    if (oldCode && ttl > expire - 60) {
        ctx.status = 204;
    } else {
        const code = generateSalt();
        // TODO: 手机邮箱验证码

        await redis.set(`send_code:${phone}`, code, 'EX', expire);
        ctx.status = 204;
    }
}

/**
 * 绑定手机号
 * @param {*} ctx koa上下文
 */
async function bindPhone(ctx) {
    const { phone, code } = ctx.request.body;
    const { user } = ctx.state;
    validateField({ phone, code });
    if (!checkThePhone(phone)) {
        ctx.throw(400, '请输入正确的手机号');
        return false;
    }
    const { redis } = ctx;
    const realCode = await redis.get(`send_code:${phone}`);
    if (code === realCode) {
        redis.del(`send_code:${phone}`);

        try {
            const count = await UserSafe.update(
                {
                    phone
                },
                {
                    where: {
                        user: user.id
                    }
                }
            );
            if (count === 0) {
                throw new Error('绑定手机号失败');
            }
            ctx.status = 205;
        } catch (e) {
            const { username } = await UserSafe.findOne({
                attributes: ['username'],
                where: {
                    phone
                }
            });
            ctx.throw(500, username ? `该手机号已被用户${username}绑定` : '绑定失败');
        }
    } else {
        ctx.throw(400, '验证码错误');
    }
}

/**
 * 绑定邮箱
 * @param {*} ctx koa上下文
 */
async function bindEmail(ctx) {
    const { email, code } = ctx.request.body;
    const { user } = ctx.state;
    validateField({ email, code });
    if (!checkTheEmail(email)) {
        ctx.throw(400, '请输入正确的邮箱');
        return false;
    }
    const { redis } = ctx;
    const realCode = await redis.get(`send_code:${email}`);
    if (code === realCode) {
        redis.del(`send_code:${email}`);

        try {
            const count = await UserSafe.update(
                {
                    email
                },
                {
                    where: {
                        user: user.id
                    }
                }
            );
            if (count === 0) {
                throw new Error('绑定邮箱失败');
            }
            ctx.status = 205;
        } catch (e) {
            const { username } = await UserSafe.findOne({
                attributes: ['username'],
                where: {
                    email
                }
            });
            ctx.throw(500, username ? `该邮箱已被用户${username}绑定` : '绑定失败');
        }
        ctx.status = 205;
    } else {
        ctx.throw(400, '验证码错误');
    }
}

/**
 * 发送邮箱验证码
 * @param {*} ctx koa上下文
 */
async function sendEmailCode(ctx) {
    const { email } = ctx.request.body;
    validateField({ email });
    if (!checkTheEmail(email)) {
        ctx.throw(400, '请输入正确的邮箱');
        return false;
    }
    const { redis } = ctx;
    const oldCode = await redis.get(`send_code:${email}`);
    const ttl = await redis.ttl(`send_code:${email}`);
    const expire = 900; // 有效期15分钟
    // 降低发送频率，间隔一分钟以上发送验证码
    if (oldCode && ttl > expire - 60) {
        ctx.status = 204;
    } else {
        const code = generateSalt();
        // TODO: 发送邮箱验证码

        await redis.set(`send_code:${email}`, code, 'EX', expire);
        ctx.status = 204;
    }
}

/**
 * 重置密码——忘记密码
 * @param {*} ctx koa上下文
 */
async function forgetPassword(ctx) {
    const { phone, code, password, confirmPassword } = ctx.request.body;
    validateField({ phone, code, password, confirmPassword });
    if (!checkThePhone(phone)) {
        ctx.throw(400, '请输入正确的手机号');
        return false;
    }
    if (password !== confirmPassword) {
        ctx.throw(400, '两次密码不一致');
        return false;
    }
    const { redis } = ctx;
    const realCode = await redis.get(`send_code:${phone}`);
    if (code === realCode) {
        redis.del(`send_code:${phone}`);

        const count = await UserSafe.update(
            {
                password
            },
            {
                where: {
                    phone
                }
            }
        );
        if (count === 0) {
            throw new Error('修改密码失败');
        }
        ctx.status = 205;
    } else {
        ctx.throw(400, '验证码错误');
    }
}

/**
 * 重置密码——修改密码
 * @param {*} ctx koa上下文
 */
async function modifiedPassword(ctx) {
    const { password, confirmPassword } = ctx.request.body;
    const { user } = ctx.state;
    validateField({ password, confirmPassword });
    if (password !== confirmPassword) {
        ctx.throw(400, '两次密码不一致');
        return false;
    }
    const count = await UserSafe.update(
        {
            password
        },
        {
            where: {
                user: user.id
            }
        }
    );
    if (count === 0) {
        throw new Error('修改密码失败');
    }
    ctx.status = 205;
}

/**
 * 重新获取token
 * @param {*} ctx koa上下文
 */
async function renewToken(ctx) {
    const { refreshToken } = ctx.query;
    try {
        const user = verifyToken(ctx, refreshToken);
        setAuthorization(
            ctx,
            {
                id: user.id
            },
            true
        );
    } catch (error) {
        ctx.throw(500, 'refreshToken无效');
    }
}

module.exports = {
    login,
    register,
    getUser,
    patchUser,
    verified,
    loginByPhone,
    bindPhone,
    validateCode,
    sendCode,
    bindEmail,
    sendEmailCode,
    forgetPassword,
    modifiedPassword,
    renewToken
};
